fix(approvals): give a restored approval suspension an issuer, so it can be decided and not only cancelled - #15966
Conversation
…can be decided and not only cancelled `AutomationEngine.restoreConsumedSuspension` re-arms the pause of a run that stranded mid-resume and tells the operator to re-issue the continuation. For an `approval` suspension nobody could. Every approvals door that stamps the resume marker — `decide`, `recall`, `sendBack`, `resubmit` — guards on a `pending` request, and the row is terminal, written by the very call that stranded the run; the generic engine door refuses an `approval` pause outright because that node declares `resumeAuthority: 'service'`. The only verb left was `cancelRun`, which discards the branch's downstream work, so the advertised repair produced a run that looked resumable and was not decidable. Measured against the real engine and the real decision door: the restored suspension lacks nothing. A `resumeAuthority`-marked resume walks the restored pause to completion. What was missing is an ISSUER on the approvals side, which is what this adds — `ApprovalService.continueRestoredRun(requestId, options?)`. The failing door now journals the signal it was carrying, on the repairable exit only (the engine's own `status: 'stranded'` discriminator, the one exit that journals a repair snapshot), under `__strandedContinuation` in the request's `node_config_json` — beside the `__decisionOutputs` side-channel already there, so no column and no authored config moves. It is awaited but can never replace the `RESUME_FAILED` throw the decision's caller is owed. Runs stranded before this shipped are served too: with no journal, the signal is rebuilt from the recorded outcome, which is exact for each accepted status because each is written by exactly one door (`approved` → `approve`, `rejected` → `reject`, `returned` → `revise`, `recalled` → `reject`). The one shape it cannot rebuild — a `rejected` row also carrying a `revise` action, possibly ADR-0044's revision-limit auto-rejection whose resume carried `autoRejected` — is refused rather than guessed. The verb replays a decision and rewrites nothing: all four `pending` guards are untouched, no status, mirror field or audit row is written, `resumeAuthority` stays as it is, and `ApprovalDecisionResult` is not touched. Like the engine verb it completes, it is an in-process operator repair with no REST route and no entry in the spec `ApprovalService` contract. Part of #15389 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
…e fake engine's insert arity The raw `resumeAuthority`-marked resume against a restored suspension is the measurement that places this card's fix on the approvals side rather than in `engine.ts`, so it is pinned in its own right and not only through the new verb. Driven on a second stranded-and-restored run so the cancel measurement above it is undisturbed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
…proval-restore-continuation
… write site and shifted lines Each repair is the one the failing gate itself prescribes, and each grows or re-anchors a ledger rather than loosening one. A base control at the merge-base (924f0fe) confirms all three are caused by this branch and were green before it. - `engine-double-contract.pinned.json` learns the two doubles (delete, update) in the new pin file. +10 lines, nothing removed, no baseline lowered: 791 pinned / 133 debt / 3 exempt, up from 789 at base. - `content/docs/permissions/system-context.mdx` re-anchored by the census's own `--fix`: inserting methods into `approval-service.ts` shifted eight cited line numbers. Pure line rot, no elevation behaviour added or removed — this branch introduces no `context.isSystem` read. - The tenant-audit census re-measured: `journalStrandedContinuation` adds one write call site, so 221 -> 222, 147 -> 148 decidable, 103 -> 104 decidably elevated. `--write` regenerates the tables; the eight prose figures the gate names as hand-written are corrected to match, each verified against the regenerated table rather than arithmetic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
…proval-restore-continuation
…tree Merging origin/main auto-merged content/docs/permissions/system-context.mdx textually clean and semantically stale: the census is green at origin/main (904e707, measured) and green on this branch before the merge, but red on the merge product with 26 problems, every one an anchor into plugin-security or rest — files this branch never touches and the merge brought in. Repaired with the census's own --fix, which re-anchors against the merged code. 15 anchors rewritten; the result reports 105 elevation read sites across 44 files and 140 resolving anchors, identical to what origin/main reports, so this restores the upstream reading rather than inventing one. No elevation behaviour is added or removed anywhere on this branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4c3cef57d168b454751cd2a47b7191c8c9a92d23 && git checkout 4c3cef57d168b454751cd2a47b7191c8c9a92d23
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2e357650306335f7fdb8939d16edce4c96b48831 cbb91f2a301f5e05cdf8f6742e620fe38fe0048b && git checkout -B drift-repro 2e357650306335f7fdb8939d16edce4c96b48831 && git merge --no-ff cbb91f2a301f5e05cdf8f6742e620fe38fe0048b
node scripts/docs-audit/affected-docs.mjs --json 2e357650306335f7fdb8939d16edce4c96b48831
|
Clause-② contract review —
|
PM — held. Rework dispatched, the
|
…proval-restore-continuation # Conflicts: # scripts/engine-double-contract.pinned.json
…ause, and refuse the rebuilds that cannot be proved
Two defects found by contract review of the first revision, both measured at the
real door, and both making the repair verb advance a flow with no decision behind
it - strictly worse than the dead end it exists to open.
GUARDS. `hasSuspendedRun(runId)` answers a boolean and cannot tell a re-armed
pause from any other live pause on the same run. Four shapes were measured
advancing something they should not: a terminal request driving a LATER approval
node's pending pause with no decision recorded on it; a plain RETRY of the verb
doing the same after the run re-suspended; a next-round `returned` row orphaning
round two; and a `recalled` request resurrected into a new pending round from a
pause it never gated. The retry is the one that settles the severity: repeating a
repair verb is the ordinary shape of using it, not a hostile construction.
Three guards now stand, each with its own reverse-control pin: `assertLatestForRun`
(already in this file, used by recall and resubmit for the same reason) refuses a
superseded row; the strict `hasSuspendedRun` refuses a run with no pause and
throws rather than read a store outage as "not suspended"; and a node-identity
check refuses a pause parked anywhere but this request's own node. The last is
fail-closed in every direction - no reader, no listing, no entry, or an entry at
another node all refuse - because a false refusal costs a retry while a false
admission advances a step nobody decided.
It needs NO new automation-engine surface. `listSuspendedRunsDurable` is already
public on AutomationEngine and already names the parked node; the approvals-side
resume interface simply declares the method it already has.
REBUILD. "Each status is written by exactly one door" was false, and it was the
load-bearing premise of the no-journal path that serves runs stranded before this
ships. `returned` has one writer but two issuers, so a stranded resubmit was
rebuilt as a send-back and walked the `revise` edge with `{decision:'revise'}`
where `{resubmitted:true}` was owed - proceeding, rather than failing loudly,
through the engine's unmatched-label fallback. It is now discriminated by the
`resubmit` action row, whose sole writer in this file is `resubmit`. `recalled`
has two writers across three behaviours, two issuing no continuation at all, and
nothing on the row tells them apart - so it is REFUSED on the rebuild path, with
a message naming what an operator can do instead. Both stay journal-recoverable.
HARNESS. The test double ignored `orderBy`, so it returned the OLDEST row where
`assertLatestForRun` asks for the newest - the guard passed on every input and a
pin naming it would have measured nothing. The double now honours it, and guard 1
is additionally driven in isolation on a row whose `created_at` is controlled, so
the ordering is what is under test rather than the clock's resolution.
PIN 3 previously asserted `decide` alone while claiming all four `pending` guards;
it now asserts all four by name.
The system-context census is regenerated from the merged tree with
`pnpm gen:system-context-census` (9 anchors), discharging the deferral the merge
commit took; it reports the same 105 sites / 44 files / 140 anchors as before.
Part of #15389
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
Clause-② contract review — round 2 —
|
Claim — round 3 reworkTaking round 3 of the rework on this PR, continuing the existing claim on #15389.
Scope, exactly as the round-3 verdict (comment 5554014979) ranks it:
⛔ Not re-opening anything round 2 settled (the four B1 refusals at the real door, the firing reverse control, Generated by Claude Code |
…d replace a phantom pin Two things this PR claimed were not what it said they were. Both were in the safe direction — nothing escalated — but a false claim is not shippable. 1. Guard 3 compared the run's parked node against the request row's own `flow_node_id`. That is right for `approve`, `reject`, `revise` and `recall`, all of which are issued at the request's own approval node. It is WRONG for `resubmit`: a resubmit is only reachable after a send-back moved the run to the revise window, and it resumes THAT pause down the `resubmit` back-edge. So a stranded resubmit re-arms at the revise window while the row still records the approval node, and guard 3 refused it — on both the journal and the rebuild paths — with a message telling the operator the pause was not this request's when it was exactly this request's. The expected node is now signal-aware (`ApprovalService.expectedPauseNode`), derived from the flow definition the same way `assertReviseEdge` already derives the revise window, fail-closed in every direction, and needing no automation surface that method did not already use. The refusal message now names the node the signal was issued from rather than "this request's own node".⚠️ It widens what guard 3 admits for exactly one signal. Every other decision resolves byte-identically to the row's own node, so the recall-in-revise-window shape stays refused: its journalled signal is `recall`, not `resubmit`. 2. PIN 7(b) could not reach the code it named. It set the reject-branch throw for a flow with no `mark_rejected` node, never decided anything, restored a suspension that had never been consumed, and asserted a refusal that was only `request is pending` — then discarded the first call with `void`, which is what hid it. It passed on every input, and P5 therefore had no pin behind it. Replaced with the real construction: strand the first approval's `approve` continuation by failing the next round's request insert once, restore, ASSERT the first call resumes and parks the run at the second approval node, then retry and assert guard 1 refuses by exact message with the second node still pending and no branch run. The test double gains a one-shot insert failure lever, which is what makes a real strand reachable from a test; every leg that uses it asserts the lever was consumed, so a fixture that silently stopped firing reads as a failure rather than as a pass. 3. PIN 8 gained two legs through `continueRestoredRun` end to end — the resubmit replay on the journal path and on the rebuild path — because its discriminator was green over code the verb never reached. PIN 7(a)'s population comment is corrected: the row is `returned`, not `recalled` (a `recalled` row is refused before any guard runs). 4. The changeset's "every one of these is journal-recoverable" is replaced by a measured, named set, with the two by-design refusals named and the two shapes no pin covers stated as not claimed. Part of #15389 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
Clause-② contract review — round 4 —
|
PM disposition — round 4 DOES NOT PASS; round 5 is queued, not droppedVerdict: comment 5554432989. Two blocking findings, and ⭐ the blocking one is the risk I ranked first in the round-4 brief — whether teaching guard 3 to accept the resubmit shape re-opens B1. It does not re-open the four. It admits a fifth. F1 — a stale journal plus a later recall advances a withdrawn requestThe four B1 constructions still refuse (P5/P3/P3b by guard 1 with the exact message, P6 by the
F2 —
|
Round 5 — taking the rework
Read in full before starting: the round-4 verdict (comment 5554432989), the PM disposition (5554441077), the round-3 verdict (5554014979), and the card comments on #15389. Scope for this round, per the disposition:
⛔ Not setting the assignee — that is the PM's field on this repo. This comment is the claim. Generated by Claude Code |
… no longer have issued The stranded-continuation journal was returned before `raw.status` was read, and nothing clears it when a later door moves the row on — so a signal could outlive the state that issued it and be replayed onto a re-armed pause. Measured, with no injected failure beyond the strand itself: a `resubmit` strands and journals `resubmit`; the submitter then recalls, a real `cancelRun` on an already-stranded run answers `false`, so the row is marked `recalled` and the run stays parked; `restoreConsumedSuspension` re-arms the pause; and `continueRestoredRun` replayed the stale `resubmit`, opening a fresh `pending` round on a request somebody deliberately withdrew — statuses `["recalled","pending"]`. Every step an ordinary action answering ordinarily. The `revise` sibling (a stranded send-back, then a recall) did the same. A row is now replayable only for a continuation its own status can have issued (`approved`→`approve`, `rejected`→`reject`, `returned`→`revise`|`resubmit`, `recalled`→`recall`), and nothing at all for a status nobody has enumerated. ⛔ Clearing the journal after a successful replay was measured NOT to close this: the offending replay is the FIRST replay of that journal, so a clear that fires afterwards can never run before the advance it would prevent. With such a clear in place both limbs still reached `resumed: true`. PIN 9 pins both limbs end to end, plus the compatible control (a stranded recall journalling `recall` on a `recalled` row still replays, and its reject branch runs) so the check reads as a gate rather than a blanket refusal. Also in this commit: - Regenerated `content/docs/permissions/system-context.mdx` with the census gate's own `--fix` (8 anchors re-anchored) and proved the result a fixpoint: a second `--fix` rewrites 0 and leaves the blob byte-identical. `pnpm check:system-context-census` exit 0. - Corrected three prose claims: "checks that the pause is genuinely re-armed" (no guard checks that — a never-consumed pause is admitted); the retracted "its journalled signal is `recall`, not `resubmit`" (a recall inside the revise window journals nothing, so the journal there is whatever an earlier strand left); and the journal-recoverable named set, which now includes the pinned `recall` journal path. - Guard 3's refusal no longer presumes a re-arm and a refusal its own PIN 7(a) population does not contain: it names the pause it was ASKED to continue against the node the outcome was ISSUED AT. Refs #15389 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
PM — round 5 accepted into review queue; three findings closed, and my brief was wrong twiceHead ⭐ It rejected the other fix shape by measuring it, not by arguingRound 4 offered two shapes for F1. The dev took the status-vs-journal compatibility table, checked before the journal is returned ( ⭐ The scope answer, with a control that firesRound 4 asked which limbs are newly admitted, which pre-existing, and whether any is live on
⭐ And the precise reading, which I would have got wrong: the signal-awareness EXPOSED P6b-ii; it did not cause it. ⛔ Two errors in my round-5 brief, both caught by the dev
⭐ And it reported a process failure that nothing would have caughtIts first M9 leg ran against an uncommitted fix, so the leg's The CI sweep, since the job halts early
F2's fixpoint is proved rather than assumed:
⛔ The round-6 contract review is queued, not run — reviews on this seat are blocked by a rate limit at the declared tier ( Generated by Claude Code |
Contract review (clause ②) — round 5 — PASS — PR #15966 at head
|
| # | claim | reading | verdict |
|---|---|---|---|
| 1 | F1 closed: a journalled signal is checked against what the row's current status can have issued before the journal is returned | resolveRecordedContinuation: CONTINUATIONS_A_STATUS_CAN_ISSUE (approved→approve, rejected→reject, `returned→revise |
resubmit, recalled→recall`, else nothing) consulted on the stashed path first; refusal names the status, the journalled signal, and the operator's two options. The other offered shape (clear on replay) was implemented and measured not to close either limb — the offending replay is the first replay. Correct placement. |
| 2 | Scope: P6b-ii newly admitted by round 3's signal-aware guard 3; P6c-ii pre-dates it; neither live on main |
Whole-tree grep for four identifiers 0/0/0/0 at merge-base and origin/main, firing control class ApprovalService = 1 — the verb ships first here, so nothing is owed beyond this PR. |
correct |
| 3 | Guard 3 stays fail-closed in every direction and does not widen beyond the resubmit signal |
expectedPauseNode read: own node for approve/reject/revise/recall; for resubmit the single approval_revise target of the node's revise edge, refusing on no engine / no getFlow / no flow / 0 or >1 windows; continueRestoredRun refuses when listSuspendedRunsDurable is absent or the parked node differs. |
correct |
| 4 | Nothing re-opens or rewrites the request row; resumeAuthority: 'service' not relaxed; ApprovalDecisionResult untouched (open ruling #15556) |
All resumes go through resumeRecordedOutcome; the four pending guards untouched; the result is its own type. |
correct |
| 5 | F2 closed: system-context.mdx regenerated at head with a proved fixpoint (8 anchors, second --fix rewrites 0) |
Lint & Repo Gates success on cbb91f2a (35 success / 0 red). The round-3 attribution to #15992 is retracted in the body. |
correct |
| 6 | F3 closed: "checks the pause is genuinely re-armed" struck; guard 3 moved out of "unchanged"; "its journalled signal is recall" retracted by name |
Changeset and body read; the changeset now states the never-consumed-pause shape as admitted-and-benign, not covered. | correct |
| 7 | Journal lives at __strandedContinuation in node_config_json, beside __decisionOutputs; best-effort, awaited, never replaces the RESUME_FAILED throw |
Read at both journalling sites (decision door on repairable, recall on stranded). |
correct |
② semver
minor (additive public surface, one fixed group). Correct.
③ Boundary flags
- Contract posture, flagged for the maintainer, not blocking.
continueRestoredRunis deliberately an in-process operator repair verb with no REST route and no entry in the specApprovalServicecontract, mirroringAutomationEngine.restoreConsumedSuspension. Under the maintainer's protocol-baseline rule the question "do operator repair verbs belong on the spec contract?" is a spec-lane decision; the PR follows the existing precedent and says so. Put to the maintainer in the director's next decision batch; whichever way it is ruled, it is a follow-on card, not a rider here. scripts/engine-double-contract.pinned.json+10 rows for the new test double — the gate's own baseline growth for a new double, gate exit 0 (731 rows). Accepted.- Journal-recoverable set is a measured, named set (approve / reject / resubmit / recall replay; reject and resubmit on the rebuild path); the
approverebuild path is stated NOT covered. Honest scoping, accepted.
Evidence and landing
Checks on cbb91f2a: 35 success / 2 skipped / 0 red; mergeable_state: clean; check-governed-merges --test on the 8 paths: 0 hits — ordinary queue landing. No needs:contract-review label was hung on this pair (the seat ran its chain in-seat); nothing to strip; this comment is the tier review of record for this head. Part of #15389, deliberately not Fixes — card stays open on merge per its own terms. Landing is the domain:services seat's on this PASS; this seat lands at its next check-in if not.
Generated by Claude Code
Part of #15389 — deliberately not
Fixes.Revision 5 answers the round-4 clause-② review (verdict): one blocking finding at the door, one blocking gate at head, and three false prose claims. Round 4 confirmed both round-3 findings fixed and that is not re-opened here. Head
cbb91f2a3.Round 5 — F1: a journalled signal outlived the state that issued it
resolveRecordedContinuationreturned the journal before it readraw.status, and nothing clears the journal when a later door moves the row on. So the verb replayed a signal the row no longer stood behind.Reproduced with no injected failure beyond the strand itself — every step an ordinary operator or submitter action answering ordinarily:
sendBack→ run parks at the revise windowreturnedresubmit, round-2 insert failed onceRESUME_FAILED,repairable: true, journal{decision:'resubmit'}recalls the stranded run — realcancelRun, no levercancelRunanswersfalse;recallreturnsresumed:falsewith noresumeError; rowrecalledrestoreConsumedSuspensionrestored: true, re-armed atwait_revisioncontinueRestoredRunresumed: true,decision:'resubmit',source:'journal'— statuses["recalled","pending"]A fresh
pendinground opened on a request somebody deliberately withdrew. Therevisesibling (a stranded send-back, then a recall) did the same, walking the run towait_revisionfor arecalledrow.The fix, and why the other offered shape is worse — measured, not argued
Two shapes were offered. Clearing the journal on a successful replay does not close either limb, and I measured that rather than reasoning about it: with such a clear implemented (blob
27466859a→0856a51ee), both limbs still reachedresumed: trueand P6b-ii still ended["recalled","pending"]. The reason is structural — the offending replay is the first replay of that journal, so a clear that fires after a replay succeeds can never run before the advance it would prevent.So the check happens before the journal is returned: a row is replayable only for a continuation its own status can have issued.
approvedapproverejectedrejectreturnedreviseorresubmitresubmitwrites no status, so the row still readsreturnedrecalledrecall⭐ Scope: which limbs are new, which predate the widening, and whether
mainis exposedThe disposition asked for this to be settled by measurement rather than assumed. It is, three ways:
resubmit) is NEWLY ADMITTED by round 3's signal-aware guard 3. Driven against the real round-2 blob8514677bd(checked out over the subject, restored to27466859awithgit diff HEAD= 0) it is refused — by guard 3,"parked at node 'wait_revision', not at request …'s own node 'review'". Same result under the M8 mutation.main, because the whole mechanism is not.git grepover the complete trees:continueRestoredRun,__strandedContinuation,resolveRecordedContinuationandexpectedPauseNodeeach match 0 files at merge-base1375344b6and 0 files onorigin/main, against 3 / 3 / 2 / 1 files at head — with a firing control (class ApprovalServicematches 1 file at both base andmain, so the search is not silently empty). This verb ships first in this PR, so nothing is owed beyond it; both limbs are this PR's to fix, and this PR fixes both.PIN 9 and its reverse controls
PIN 9 pins both limbs end to end, plus the control that keeps it from reading as "a
recalledrow is refused":wait_revision, still cancellable.recalljournallingrecallon arecalledrow still replays —resumed: true,source:'journal', andmarks=['mark_rejected'], so the reject branch it was carrying actually ran. The check gates; it does not blanket-refuse. (This also newly pins therecalljournal path, which round 3's changeset listed as not covered by a pin.)recalldoes not throw its strand — it reports throughresumeErrorand returns. An earlier draft of (c) asserted a throw and measured nothing; the strand is now asserted as theresumeErrorplus a consumed suspension.if (false))refusedStaleisnullexpectedPauseNode's signal-awareness removedRound 5 — F2:
check:system-context-censuswas exit 1 at head, and it is this PR's⛔ Correcting the round-3 report. It said "81 gates, 80 exit 0, 1 non-zero (
react-declaration-parity)". That undercounts its own reds, and the miscount has a second half worth naming:dispatch-gates.mjsoutput). It went unreported.check:react-declaration-parityis not in that list at all. It appears under "Artifact rosters — 36 families whosesilentverdict is a fact about a LIST, not about your paths". The local-gate section holds 80 entries, of which the census is two.So the honest reading at head was two non-zero, not one, and the one that was named was not even in the population being counted.
Cause: round 3's own hunk
@@ -163,8 +163,10 @@inserted two docstring lines above eightcontext.isSystemreads, andcontent/docs/permissions/system-context.mdx— amerge=os-regenpath — was not regenerated. ⛔ Not the base-branch failure: the failing CI step is 8, and the merge-driver gate at step 141 is skipped on that job.Fixpoint proved, not asserted — for a
merge=os-regenpath no tree state is trustworthy until the generator re-runs on it:check-system-context-census.mjs --fix(=pnpm gen:system-context-census)20421d525→5ecbdbb16check-system-context-census.mjs(plain)--fixagainpnpm check:system-context-census(the real gate, self-test included)Round 5 — F3: three false prose claims, corrected identically here and in the changeset
returnedrow with aresubmitaction row and a never-consumed pause is admitted, withrestoreConsumedSuspensionitself answering "already resumable — nothing to restore". The changeset now states what the guards actually check, and names that shape as benign-but-untested rather than implying coverage.recall, notresubmit" — false, and it was the reasoning behind the hole. A recall taken inside the revise window callscancelRunand journals nothing, so the journal on such a row is whatever an earlier strand left there — most often aresubmit, since the resubmit is what that window exists to receive. The docstring now retracts the claim by name and points at what actually refuses the shape.Nit, fixed. Guard 3's refusal ended "this re-armed pause is not the one that outcome was refused on" — presuming a re-arm and a refusal that PIN 7(a)'s population does not contain (its send-back landed; nothing was re-armed, nothing was refused). It now reads: "so the pause this verb was asked to continue is not the one that outcome was issued at". The same presumption is gone from the
listSuspendedRunsDurableinterface docstring, and PIN 7(a)'s title and comment say so.What rounds 1–2 established, unchanged and not re-opened
assertLatestForRun(raw)— refuses a superseded row.hasSuspendedRun— a pause exists at all. Strict: an unreadable store throws rather than reading as "not suspended".Round 4 independently re-confirmed, and this revision does not touch: the four B1 constructions still refuse (P5/P3/P3b by guard 1 with its exact message, P6 by the
recalledrebuild refusal) with the reverse control still firing;expectedPauseNodefail-closed across eleven limbs with 0 resume calls; PIN 7(b) real (M4 reds it; with the double's lever disabled it reds at:484); and B2's discriminator legs reddening only what they should.Verification
Every exit code captured right after a single redirected command, never off a pipe; each Bash call carried its own
cd. Heavy builds serialized throughos-verify-lock.shand read from itsVERDICTline.Build first.
pnpm --filter '@objectstack/plugin-approvals^...' buildunder the lock —VERDICT command-exit 0, held 307s.Pins.
Test Files 1 passed (1) · Tests 9 passed (9), exit 0.Suite.
pnpm --filter @objectstack/plugin-approvals test—Test Files 40 passed (40) · Tests 684 passed (684), exit 0 (round 4's 40/683, plus PIN 9).typecheckexit 0,check:test-typecheck: OK — 8 file(s) / 324 error(s) / 27 pinned signature(s) held, unchanged.Mutation discipline. Subject
packages/plugins/plugin-approvals/src/approval-service.ts, mutated after the implementation was committed. Each anchor asserted to occur exactly once as an exact substring in the form written; mutation proved on disk by agit hash-objectdelta plus marker counts (removed text 0, injected text 1); restore viagit checkout HEAD -- ABSOLUTE-PATHundertrap … EXIT INT TERM, proved by blob equality againstHEAD:THE-SUBJECT-PATHand an emptygit diff HEAD. The pin file imports./approval-service.js— a relative specifier inside the same package — so the subject resolves fromsrc, notdist; no rebuild sits between mutation and reading.git checkout HEAD -- …restore leg deleted the fix itself — exit 0, no diagnostic. The fix was re-applied and proved byte-identical (d20821911) before anything else ran, and every leg reported here was run against the committed tree. This is exactly the failure the "commit before reverse-verifying" rule names.Gates. Family re-derived from the actual changed files:⚠️ It reports a STALE TREE warning (this branch is behind
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, exit 0.origin/main, and files the derivation reads changed in that range) — I am instructed not to mergemain, so instead I derived twice, 8 commits oforigin/mainapart, and the local-gate list came back byte-identical. The staleness did not change the answer for this diff; that is measured, not assumed.Gates — the whole runnable answer, 93 of 93 exit 0, at⚠️ And the population itself was wrong before:
cbb91f2a3.dispatch-gates.mjsprints its own Reconciliation line — "93 famil(ies): this card's WHOLE runnable answer … 80 named by PATH + 8 by change KIND + 7 DECLARED whole-tree, 2 reached both ways" — and warns in-band that harvesting one block yields 80 and "reports nothing missing". Round 3's "81" matched neither. Harvested this time with--commands(the script's own one-command-per-line form, which cannot drop a spelling): 93 commands, 93 exit 0, each run bare with its exit captured immediately after a single redirect.⭐ The steps CI never reached, swept — because a skipped step is UNMEASURED, not green.
Lint & Repo Gateshalts at GitHub step 8 (declared step 7, "Docs anchors resolve to real headings"), so steps 9–234 never ran. All 213 distinct commands in that region were extracted fromlint.ymland run locally: 211 exit 0, and 2 exit 124 — both my own 300s harness cap, not gate verdicts. ⛔ Neither is reported as a pass; both were re-run without the cap:pnpm check:pm-dispatch-gatesdispatch-gates self-test: 1511 cases passturbo run typecheck --filter='./packages/*' --filter='./packages/*/*' --filter='./apps/*'Tasks: 135 successful, 135 total(under the verify lock,VERDICT command-exit 0)⇒ 213 of 213 exit 0. The failing step itself is green in full now:
pnpm check:doc-anchors0, census--self-test0, census 0.NOT MEASURED, named rather than implied:
check:react-declaration-parity— exit 1, and it is a prerequisite refusal, not a red on this branch: "MANIFEST is not set — there is no registry side to compare against. This gate did NOT run. That is a failure, not a skip (check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690)." The registry side is objectui'ssdui.manifest.json, which this repo contains no copy of. Not attributable here:git diff --name-only 1375344b6..HEAD -- packages/specis empty.check:single-claim-paths(the self-test form is exit 0 and is what the npm script runs; the GitHub-querying form is untried here).service-automationwas not run: this PR still changes 0 files in it.⛔ Retracted from the round-3 body: "
Lint & Repo Gatesis expected red independently of this branch (#15992)." That was wrong. The failing step is 8, the merge-driver gate at step 141 is skipped on that job, andmainis green — the red was this PR's own stalemerge=os-regenartifact, fixed above.